BinaryBuilder

Pkg + BinaryBuilder

原文戳我

Pkg相关章节

BinaryBuilder

Hack

  • BinaryBuilderYggdrasil是用Julia维护(编译)第三方二进制依赖的最佳方案, 学习它们, 从而方便基于julia实现复杂轮子的复用;

  • 专注于linux/MacOSx86_64平台的编译, Windows平台的正在开发中;

使用场景 假设你的Foo.jl包, 需要依赖libfoo共享库, 正常的开发流程是:

  1. 在自己电脑上编译libfoo, 然后用Libdl.open()打开对应的库;

  2. ccall()调用导出的函数, 然后调试、编写C绑定;

  3. 分享自己的库;

在第三步的时候, BinaryBuilder.jl就可以派上用场了, 它可以自动帮你构建所有依赖项的编译版本, 而且会封装成一个Julia包装器包(JLL Package), 方便进行依赖的安装、版本控制、以及本地化。

  • BinaryBuilder首先是创建一个编译配方, 通常命名为build_tarballs.jl, 可以理解为julia版本的makefile, 里边记录了构建名称、版本、源等信息, 以及实际构建步骤;

  • Julia社区维护Yggdrasil仓库专门用来存放配方(Yggdrasil=>世界之树=>伊格德拉斯尔), 里边也有很多如何编写bulid_tarballs.jl的示例;

  • 成功构建的结果是一个JLL包, 通常会上传到JuliaBinaryWrappers, 并在Julia的General注册表中添加注册请求, 从而用户可以通过add libfoo_jll来添加依赖;

构建包

构建贴士

JLL包

JLLDLL(Dynamic-Link Library)的双关语, J for Julia

JLL包的结构

JLL包的内部结构实际上并不复杂, 一个典型的JLL结构:

julia

NAME_jll
├── Artifacts.toml
├── LICENSE
├── Project.toml
├── README.md
└── src/
    ├── NAME_jll.jl
    └── wrappers/
        ├── aarch64-linux-gnu.jl
        ├── aarch64-linux-musl.jl
        ├── armv7l-linux-gnueabihf.jl
        ├── armv7l-linux-musleabihf.jl
        ├── i686-linux-gnu.jl
        ├── i686-linux-musl.jl
        ├── i686-w64-mingw32.jl
        ├── powerpc64le-linux-gnu.jl
        ├── x86_64-apple-darwin14.jl
        ├── x86_64-linux-gnu.jl
        ├── x86_64-linux-musl.jl
        ├── x86_64-unknown-freebsd11.1.jl
        └── x86_64-w64-mingw32.jl

julia
  • LICENSE,README.md,*.toml是常规文件, 略;

  • src/NAME_jll.jl是软件包的主要入口, 在输入using NAME_jll时执行;

  • src/wrappers/目录记录支持的不同平台;

wrappers

src/wrappers是自动生成的包装器, 加载当前JLL的依赖JLL, 并到处生成当前JLL的build_tarballs.jl所列出的产品名, 同时还定义以下不导出的变量:

  • artifact_dir: 当前二进制文件的安装目录(绝对路径);

  • PATH: 执行当前JLL所需要的PATH环境(如果需要的话);

  • PATH_list: 以向量形式存储的PATH中的目录Vector{String};

  • LIBPATH: 搜索依赖库的PATH;

  • LIBPATH_list: 依赖库PATH的Vector;

每个包装文件还定义了__init__()功能, 设置每次加载包时执行的代码;

LibraryProduct

LibraryProduct <: Product类型表示共享库, 可以用ccall调用, 假设一个名为libdataproc的jll, 包装器会定义如下变量:

  • 常量libdataproc, 可以被ccall调用: num_chars = ccall((:count_characters, libdataproc), Cint, (Cstring, Cint), data_lines[1], length(data_lines[1]))

  • libdataproc_path: 共享库的绝对路径, 不是常量, 所以不能被ccall调用;

  • libdataproc_handle: 共享库在内存中的地址;

ExecutableProduct

ExecutableProduct <: Product类型是在当前平台上可运行的二进制文件, 假设一个名为mungify_exe的可执行文件, 可以在包装器中以如下两种方式调用:

  1. 以插值的形式直接运行函数, 虽然代码不太易读, 但是是线程安全的, 所以首推这种方式:

julia

# Only available in Julia v1.6+
run(`$(mungify_exe()) $arguments`)

julia
  1. do...end代码块包裹, 分配函数名, 再插值调用, 更清晰:

julia

mungify_exe() do exe
    run(`$exe $arguments`)
end

julia
为什么需要可执行文件的包装器? 我直接run()中加可执行文件的绝对路径就行了呗?

  • 用包装器的好处就是确保在运行时找到所需的所有共享库(感觉这时有点像linux的module命令了)

FileProduct

FileProduct <: Product对应简单文件, 记录文件的绝对路径:

julia

# 假设有一个FileProduct叫data_txt
data_lines = open(data_txt, "r") do io
    readlines(io)
end
# 还可以用data_txt_path来输出文件的绝对路径, 跟data_txt实际上是一样的, 只是为了与上述两种Product语法对齐

julia

覆盖JLL

JLL利用Artifacts来工作, 可以用Overrides.toml来配置用自己的可执行文件/库/文件来覆盖JLL, 根据包是否被dev, 可以分三种覆盖方式。

覆盖deved JLL Packages

覆盖Non-deved JLPackages

覆盖JLL中的特定内容

常见问题